package hotnet.nio;

import hotnet.config.DefaultIoSocketSessionConfig;
import hotnet.config.IoSessionConfig;
import hotnet.filter.IoFilter;
import hotnet.filter.IoFilterChain.Entry;
import hotnet.processor.IoProcessor;
import hotnet.service.AbstractPollingIoConnector;
import hotnet.service.SocketConnector;
import hotnet.session.IoSession;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public class NioSocketConnector extends AbstractPollingIoConnector implements SocketConnector {
	private volatile Selector selector;
	
	public NioSocketConnector() {
        super(new DefaultIoSocketSessionConfig(), NioSocketProcessor.class);
    }

	
	public NioSocketConnector(IoSessionConfig config, Class<? extends IoProcessor> processorCls) {
		super(config, processorCls);
	}

	@Override
	protected void init() throws Exception {
		this.selector = Selector.open();
		
	}

	@Override
	protected void destroy() throws Exception {
		if (this.selector != null) {
			selector.close();
		}
	}

	@Override
	protected SocketChannel newChannel(SocketAddress localAddress)
			throws Exception {
		SocketChannel channel = SocketChannel.open();
		int recvBufSize = getSessionConfig().getReadBufferSize();
		
		if (recvBufSize > 65535) {
            channel.socket().setReceiveBufferSize(recvBufSize);
        }
		
		if (localAddress != null) {
			try {
				channel.socket().bind(localAddress);
			} catch (IOException e) {
				channel.close();
				String newMessage = "Error while binding on " + localAddress + "\n" + "original message : "
                        + e.getMessage();
                Exception e1 = new IOException(newMessage);
                e1.initCause(e.getCause());
				
				throw e1;
			}
		}
		
		channel.configureBlocking(false);
		
		return channel;
	}

	@Override
	protected boolean connect(SocketChannel handle, SocketAddress remoteAddress)
			throws Exception {
		return handle.connect(remoteAddress);
	}

	@Override
	protected void close(SocketChannel channel) throws Exception {
		SelectionKey key = channel.keyFor(selector);
		
		if (key != null && key.isValid()) 
			key.cancel();
		
		channel.close();
	}

	@Override
	protected boolean finishConnect(SocketChannel handle) throws Exception {
		if (handle.finishConnect()) {
			SelectionKey key = handle.keyFor(selector);
			
			if (key != null) {
                key.cancel();
            }

            return true;
		}
		
		return false;
	}

	@Override
	protected IoSession newSession(IoProcessor processor, SocketChannel handle)
			throws Exception {
		IoSession session = new NioSocketSession(processor, handle, this);

		return session;
	}

	@Override
	protected void wakeup() {
		selector.wakeup();
		
	}

	@Override
	protected int select(int timeout) throws Exception {
		return selector.select(timeout);
	}

	@Override
	protected Iterator<SocketChannel> selectedHandles() {
		return new SocketChannelIterator(selector.selectedKeys());
	}

	@Override
	protected Iterator<SocketChannel> allHandles() {
		return new SocketChannelIterator(selector.keys());
	}

	@Override
	protected void register(SocketChannel channel, ConnectionRequest request)
			throws Exception {
		channel.register(selector, SelectionKey.OP_CONNECT, request);
		
	}

	@Override
	protected ConnectionRequest getConnectionRequest(SocketChannel handle) {
		SelectionKey key = handle.keyFor(selector);

		if (key == null || !key.isValid()) {
			return null;
		}
		
		return (ConnectionRequest)key.attachment();
	}

	private class SocketChannelIterator implements Iterator<SocketChannel> {
		private final Iterator<SelectionKey> i;
		
		public SocketChannelIterator(Collection<SelectionKey> selectedKeys) {
			this.i = selectedKeys.iterator();
		}
		
		@Override
		public boolean hasNext() {
			return this.i.hasNext();
		}

		@Override
		public SocketChannel next() {
			return (SocketChannel)(i.next().channel());
		}

		@Override
		public void remove() {
			i.remove();
			
		}
		
	}

}
